import React, {Component} from 'react'
import {MarkdownPreview} from 'react-marked-markdown';
// import py1_1 from '../static/py1_1.md'
// import marked from 'marked';

class Py27 extends Component {
    constructor(props) {
        super(props);
        this.state = {
           value: `
当研发一个技术栈较大的网络项目时，经常只对栈底（项目后期用的技术）进行一些常规
测试。目前大多数编程语言（包括 Python）都有一些测试框架，但是网站的前端通常并没
有自动化测试，尽管前端才是整个项目中真正与用户零距离接触的唯一一个部分。

部分原因是网站常常是不同标记语言和编程语言的大杂烩。你可以为 JavaScript 部分写单
元测试，但没什么用，如果 JavaScript 交互的 HTML 内容改变了，那么即使 JavaScript 可
以正常地运行，也不能完成网页需要的动作。

网站前端测试经常被当作一件放到最后才做的事情，或者指派给低级程序员去做，最多再
给他们一个检查表和一个 bug 跟踪器。但其实只要再稍微努点儿力，我们就可以把检查表
变成单元测试，用网络爬虫代替人眼进行测试。

想象有一个由测试驱动的网络开发项目。每天进行测试以保证网络接口的每个环节的功能
都是正常的。每当有新的特性加入网站，或者一个元素的位置改变时，就执行一组自动化
测试。在这一章里，我将介绍测试的基础知识，以及如何用 Python 网络爬虫测试各种简单
或复杂的网站。

> 13.1　测试简介

如果你以前从来没有为你的代码写过测试，那么现在开始再合适不过了。运行一套测试方
法能够保证你的代码按照既定的目标运行，不仅可以节约你的时间，减少你对 bug 的忧
虑，还可以让新版本升级变得更加简单。

#### 什么是单元测试

测试和单元测试（unit test）这两个词基本可以看成是等价的。一般当程序员们说起“写测
试”时，他们真正的意思就是“写单元测试”。而一些程序员提到写单元测试时，他们写
的就是某一种测试。

虽然不同公司的单元测试定义和实践方法大相径庭，但是一个单元测试通常包含以下
特点。
* 每个单元测试用于测试一个零件（component）功能的一个方面。例如，如果从银行账
户取出一笔金额为负数的美元，那么单元测试就要对负数抛出适当的错误信息。
通常，一个零件的所有单元测试都集成在同一个类（class）里。你可能有一个测试是针
对从银行账户取出一笔金额为负数的美元，另一个测试是针对透支银行账户行为的单元
测试。
* 每个单元测试都可以完全独立地运行，一个单元测试需要的所有启动（setup）和卸载
（teardown）都必须通过这个单元测试本身去处理。单元测试不能对其他测试造成干扰，
而且不论按何种顺序排列，它们都必须能够正常地运行。
* 的和是 4。有时，一个单元测试也许只包含一个失败状态（failure state）。例如，有这样
一个单元测试，如果一个异常没有被抛出，测试失败；如果每个异常都顺利抛出，测试
通过。
* 单元测试与生产代码是分离的。虽然它们需要导入然后在待测试的代码中使用，但是它
们一般被保留在独立的类和目录中。

尽管有很多测试类型可以大写特写，像整体测试（integration test）和有效性测试
（validation test）等，但本章只重点介绍单元测试。这不仅仅是因为单元测试在当前的测试
驱动开发中十分主流，还因为其代码长度和灵活性使它们非常适合做示例。另外 Python 自
带单元测试标准库，我们下一节就来介绍它。

> 13.2　Python单元测试

Python 的单元测试模块 unittest ，所有标准版 Python 安装后都有。只要先导入模块然后
继承 unittest.TestCase 类，就可以实现下面的功能：
* 为每个单元测试的开始和结束提供 setUp 和 tearDown 函数
* 提供不同类型的“断言”语句让测试成功或失败
* 把所有以 test_ 开头的函数当作单元测试运行，忽略不带 test_ 的函数

下面的例子演示了如何用 Python 实现一个非常简单的单元测试，测试 2+2=4：
 
    import unittest

    class TestAddition(unittest.TestCase):
        def setUp(self):
            print("Setting up the test")

        def tearDown(self):
            print("Tearing down the test")

        def test_twoPlusTwo(self):
            total = 2+2
            self.assertEqual(4, total)
    if __name__ == '__main__':
        unittest.main()
 
虽然 setUp 和 tearDown 函数在这里并没有实现可用的功能，但是仍然达到了演示的目的。
需要注意的是，这两个函数在每个测试的开始和结束都会运行一次，而不是把类中所有测
试作为一个整体在开始或结束时各运行一次。

#### 测试维基百科

将 Python 的 unittest 库与网络爬虫组合起来，就可以实现简单的网站前端功能测试（除
了 JavaScript 测试，后面我们会介绍）。
  
    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import unittest

    class TestWikipedia(unittest.TestCase):
        bsObj = None
        def setUpClass():
            global bsObj
            url = "http://en.wikipedia.org/wiki/Monty_Python"
            bsObj = BeautifulSoup(urlopen(url))
            
        def test_titleText(self):
            global bsObj
            pageTitle = bsObj.find("h1").get_text()
            self.assertEqual("Monty Python", pageTitle)

        def test_contentExists(self):
            global bsObj
            content = bsObj.find("div",{"id":"mw-content-text"})
            self.assertIsNotNone(content)
    if __name__ == '__main__':
        unittest.main()
  
这里有两个测试：第一个是测试页面的标题是否为“Monty Python”，另一个是测试页面是
否有一个 div 节点 id 属性是 "mw-content-text" 。

需要注意的是，这个页面的内容只加载一次，全局对象 bsObj 由多个测试共享。这是通过
unittest 类的函数 setUpClass 来实现的，这个函数只在类的初始化阶段运行一次（与每个
测试启动时都运行的 setUp 函数不同）。用 setUpClass 代替 setUp 可以省去不必要的页面
加载；我们可以一次性采集全部内容，供多个测试使用。

虽然一次只测试一个页面看起来可能不够强大，也没什么意思，但是如果你还记得第 3 章
的内容，就可以轻松地创建一个网络爬虫去遍历网站中所有的页面。下面我们来看看，当
把网络爬虫和一个向页面内容添加断言的单元测试组合起来时，会发生什么呢？

有很多方法可以重复执行一个测试，但是我们必须对即将在页面上运行的所有测试都时刻
保持谨慎，因为我们只加载一次页面，而且我们必须避免在内存中一次性加入大量的信
息。具体设置如下所示：
 
    class TestWikipedia(unittest.TestCase):
        bsObj = None
        url = None

        def test_PageProperties(self):
            global bsObj
            global url

            url = "http://en.wikipedia.org/wiki/Monty_Python"
            # 测试遇到的前100个页面
            for i in range(1, 100):
                bsObj = BeautifulSoup(urlopen(url))
                titles = self.titleMatchesURL()
                self.assertEquals(titles[0], titles[1])
                self.assertTrue(self.contentExists())
                url = self.getNextLink()
            print("Done!")

        def titleMatchesURL(self):
            global bsObj
            global url
            pageTitle = bsObj.find("h1").get_text()
            urlTitle = url[(url.index("/wiki/")+6):]
            urlTitle = urlTitle.replace("_", " ")
            urlTitle = unquote(urlTitle)
            return [pageTitle.lower(), urlTitle.lower()]

        def contentExists(self):
            global bsObj
            content = bsObj.find("div",{"id":"mw-content-text"})
            if content is not None:
                return True
            return False

        def getNextLink(self):
        #使用第5章介绍的方法返回随机链接
    if __name__ == '__main__':
        unittest.main()
 
有几个地方需要注意。首先，这个类里面实际上只有一个测试。其他的函数其实都是功能
性的辅助函数（helper function），即使它们做了大量计算来判断测试是否通过。因为测试
函数 test_PageProperties 使用了断言语句，所以测试的结果最终会传到这些断言所在的
函数里。

另外， contentExists 返回的是布尔变量， titleMatchesURL 返回的是字符串列表。我们之
所以用列表而不用布尔变量作为断言语句的值，其原因如下所示：
 
    ======================================================================
    FAIL: test_PageProperties (__main__.TestWikipedia)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
        File "15-3.py", line 22, in test_PageProperties
            self.assertTrue(self.titleMatchesURL())
    AssertionError: False is not true
 
assertEquals 语句的结果是：
   
    ======================================================================
    FAIL: test_PageProperties (__main__.TestWikipedia)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
        File "15-3.py", line 23, in test_PageProperties
            self.assertEquals(titles[0], titles[1])
    AssertionError: 'lockheed u-2' != 'u-2 spy plane'
   
究竟哪一种方式调试起来更方便呢？

在这个示例中，之所生会发生错误是因为网页发生了一个重定向，即词条 https://en.wikipedia.org/wiki/U-2_spy_plane 会跳转到标题为“Lockheed U-2”的词条。

> 13.3　Selenium单元测试

和第 10 章里介绍的 Ajax 采集一样，在网站测试中 JavaScript 也是一个难题。幸运的是，
我们有 Selenium，它是一个可以解决网站上各种复杂问题的优秀测试框架；其实，它的初
衷就是用来做网站测试！

虽然这里的单元测试都是同一种语言（Python）写的，但是 Python 单元测试和 Selenium
单元测试的语法还是有点儿不一样。Selenium 不要求单元测试必须是类的一个函数，它的
“断言”语句也不需要括号，而且测试通过的话不会有提示，只有当测试失败时才会产生
信息提示：
   
    driver = webdriver.PhantomJS()
    driver.get("http://en.wikipedia.org/wiki/Monty_Python")
    assert "Monty Python" in driver.title
    driver.close()
    
当这段代码运行的时候，测试结果不会输出任何信息。

因此，写 Selenium 单元测试的时候需要比写 Python 单元测试更加随意，断言语句甚至可
以整合到生产代码中，非常适合某些条件不能满足就中断代码的需求。

#### 与网站进行交互

最近，我想通过一个网站的商户通讯录联系我家附近的一个小商家，结果发现网页上的信
息没有了；我点击提交按钮的时候什么也没查到。经过一些探索之后，我发现这个网站用
了一个简易的邮件发送表单，如果商户联系方式的内容有问题就可以给网管发邮件。于是
我就用这个邮箱地址给他们发了一封邮件，告诉他们联系方式信息表单出了问题，让他们
尽快解决，虽然不是技术问题。

如果我要写一个普通的爬虫来采集或测试这个表单，那么爬虫也许只能复制表单的结构，
然后直接给我自己发邮件——不过抓不到表单的内容。那么我怎么测试表单的功能才能保
证它在浏览器上也可以正常工作呢？

虽然在前面的几章中我们介绍过链接跳转、表单提交和其他网站交互行为，但是我们做那
些事情的共同初衷都是要避开浏览器图形界面，而不是使用浏览器。另一方面，Selenium
可以在浏览器（这里用 PhantomJS 无头浏览器）上做任何事，包括输入文字、点击按钮
等，这样就可以找出异常表单、JavaScript 代码错误、HTML 排版错误，以及其他用户使
用过程中可能出现的问题。

这个测试的关键是使用 Selenium 的 elements 。这个对象在第 10 章已经简单介绍过了，它
的调用方式如下所示：
    
    usernameField = driver.find_element_by_name('username')
     
就像你可以在浏览器里对网站上的不同元素执行一系列操作一样，Selenium 也可以对任何
给定元素执行很多操作，如下所示：
    
    myElement.click()
    myElement.click_and_hold()
    myElement.release()
    myElement.double_click()
    myElement.send_keys_to_element("content to enter")
    
为了一次性完成一个元素的多个操作，可以用动作链（action chain）储存多个操作，然后
在一个程序中执行一次或多次。用动作链储存多个操作非常方便，而且非常有用，它们的
功能和前面示例中对一个元素显式调用操作是完全一样的。

为了演示两种方式的差异，我们看一看 http://pythonscraping.com/pages/files/form.html 的表
单。我们用下面的方式填写表单并提交：
  
    from selenium import webdriver
    from selenium.webdriver.remote.webelement import WebElement
    from selenium.webdriver.common.keys import Keys
    from selenium.webdriver import ActionChains

    driver = webdriver.PhantomJS(executable_path='<Path to Phantom JS>')
    driver.get("http://pythonscraping.com/pages/files/form.html")

    firstnameField = driver.find_element_by_name("firstname")
    lastnameField = driver.find_element_by_name("lastname")
    submitButton = driver.find_element_by_id("submit")

    ### 方法1 ###
    firstnameField.send_keys("Ryan")
    lastnameField.send_keys("Mitchell")
    submitButton.click()
    ################

    ### 方法2 ###
    actions = ActionChains(driver).click(firstnameField).send_keys("Ryan")
                                    .click(lastnameField).send_keys("Mitchell")
                                    .send_keys(Keys.RETURN)
    actions.perform()
    ################

    print(driver.find_element_by_tag_name("body").text)

    driver.close()
    
方法 1 在两个字段上调用 send_keys ，然后点击确认按钮；而方法 2 在用一个动作链来点
击每个字段并填写内容，最后确认，这些行为是在 perform 调用之后才发生的。无论用第
一个方法还是第二个方法，这个程序的结果都一样：
    
    Hello there, Ryan Mitchell!
     
两个方法除了处理命令的对象不同之外，第二个方法还有一点差异：注意第一个方法提交
表单是点击“确认”按钮，而第二个方法提交表单是用回车键（ Keys.RETURN ）。因为实现同
样效果的网络事件发生顺序可以有多种，所以用 Selenium 实现同样的结果也有许多方式。

1. 鼠标拖放动作

单击按钮和输入文字只是 Selenium 的一个功能，其真正的亮点是能够处理更加复杂的网络
表单交互行为。Selenium 可以轻松地完成鼠标拖放动作（drag-and-drop）。使用它的拖放函
数，你需要指定一个被拖放的元素以及拖放的距离，或者元素将被拖放到的目标元素。

下面的例子用 http://pythonscraping.com/pages/javascript/draggableDemo.html 页面演示了拖
放动作：
   
    from selenium import webdriver
    from selenium.webdriver.remote.webelement import WebElement
    from selenium.webdriver import ActionChains

    driver = webdriver.PhantomJS(executable_path='<Path to Phantom JS>')
    driver.get('http://pythonscraping.com/pages/javascript/draggableDemo.html')

    print(driver.find_element_by_id("message").text)

    element = driver.find_element_by_id("draggable")
    target = driver.find_element_by_id("div2")
    actions = ActionChains(driver)
    actions.drag_and_drop(element, target).perform()

    print(driver.find_element_by_id("message").text)
   
示例页面的 message 节点上显示了两条信息。第一条是：
    
    Prove you are not a bot, by dragging the square from the blue area to the redarea!
    
然后任务很快就会完成，第二条内容就被打印出来：
    
    You are definitely not a bot!
     
当然，就像示例页面上显示的，很多验证码里使用拖放动作证明访问者不是一个机器
人，这是一种常用手段。虽然机器人也可以长时间拖着一个元素不放（就是点击，拖
住，移动），但是也不知道为什么，用“拖住不放”来检验一个用户是不是机器人的方
式仍然存在。

另外，这些可拖放的验证码库很少使用那些“能够难住机器人”的任务，比如“拖动小猫
图片放到奶牛图片的上面”（这需要你能够识别“小猫”和“奶牛”图片）；相反，它们经
常用数字排序或其他一些非常不起眼的任务，就像前面例子里的拖放。
当然，这些验证码库的优势在于那些简单任务可以实现大量的变化，而且每种变化的使用
频率都不高——另外也不会有人愿意花时间去做一个能够搞定所有任务的机器人。至少这
个例子可以解释为什么你不应该在大型网站上用这种技术。

2. 截屏

除了普通的测试功能，Selenium 还有一个有趣的技巧可以让你的测试更容易（或者让你老
板更喜欢）：截屏。截屏可以在单元测试中创建，不需要点击截屏按钮就可以获取：
  
    driver = webdriver.PhantomJS()
    driver.get('http://www.pythonscraping.com/')
    driver.get_screenshot_as_file('tmp/pythonscraping.png')
  
这段脚本会访问 http://pythonscraping.com/，并将主页的屏幕截图保存在本地的 tmp 文件夹
中（该文件夹必须创建好，以供正确存储之用）。截屏可保存为多种文件格式。

>  13.4　Python单元测试与Selenium单元测试的选择

Python 的单元测试语法严谨冗长，更适合为大多数大型项目写测试，而 Selenium 的测试方
式灵活且功能强大，可以成为一些网站功能测试的首选。那么应该使用哪个呢？

答案是：不需要选择。Selenium 可以轻易地获取网站的信息，而单元测试可以评估这些信
息是否满足通过测试的条件。因此，你没有理由拒绝把 Selenium 导入 Python 的单元测试，
两者组合是最佳拍档。

例如，下面的程序创建了一个带拖放动作的网站单元测试，如果一个元素被正确地拖放到
另一个元素里，那么推断条件成立，会显示“你不是一个机器人！”（You are not a bot!），
测试通过。
 
    from selenium import webdriver
    from selenium.webdriver.remote.webelement import WebElement
    from selenium.webdriver import ActionChains
    import unittest

    class TestAddition(unittest.TestCase):
        driver = None
        def setUp(self):
            global driver
            driver = webdriver.PhantomJS(executable_path='<Path to Phantom JS>')
            url = 'http://pythonscraping.com/pages/javascript/draggableDemo.html'
            driver.get(url)

        def tearDown(self):
            print("Tearing down the test")

        def test_drag(self):
            global driver
            element = driver.find_element_by_id("draggable")
            target = driver.find_element_by_id("div2")
            actions = ActionChains(driver)
            actions.drag_and_drop(element, target).perform()

            self.assertEqual("You are definitely not a bot!", driver.find_element_by_id("message").text)
    if __name__ == '__main__':
        unittest.main()
   

任何网站上可以看到的内容都可以通过 Python 的单元测试和 Selenium 组合来测试。其
实，如果再与第 11 章介绍的一些图像处理库组合起来，就可以通过网站截屏实现像素级
测试了！
 
    
           
           `,
        };
      }
    render() {
        return (
            <div >
                {/* 111111111111111
                <Py112 /> */}
                <MarkdownPreview value={this.state.value}/>
            </div>
        )
    }
}

export default Py27